diff --git a/swh/web/ui/converters.py b/swh/web/ui/converters.py --- a/swh/web/ui/converters.py +++ b/swh/web/ui/converters.py @@ -181,6 +181,8 @@ dates={'date', 'committer_date'}) if revision: + if 'parents' in revision: + revision['merge'] = len(revision['parents']) > 1 if 'message' in revision: try: revision['message'] = revision['message'].decode('utf-8') diff --git a/swh/web/ui/main.py b/swh/web/ui/main.py --- a/swh/web/ui/main.py +++ b/swh/web/ui/main.py @@ -11,6 +11,7 @@ from swh.web.ui.renderers import RENDERERS, urlize_api_links from swh.web.ui.renderers import safe_docstring_display +from swh.web.ui.renderers import revision_id_from_url from swh.storage import get_storage @@ -32,7 +33,7 @@ app = FlaskAPI(__name__) app.jinja_env.filters['urlize_api_links'] = urlize_api_links app.jinja_env.filters['safe_docstring_display'] = safe_docstring_display - +app.jinja_env.filters['revision_id_from_url'] = revision_id_from_url AUTODOC_ENDPOINT_INSTALLED = False diff --git a/swh/web/ui/renderers.py b/swh/web/ui/renderers.py --- a/swh/web/ui/renderers.py +++ b/swh/web/ui/renderers.py @@ -92,6 +92,12 @@ return re.sub(src, dest, docstring) +def revision_id_from_url(url): + """Utility function to obtain a revision's ID from its browsing URL.""" + return re.sub(r'/browse/revision/([0-9a-f]{40}|[0-9a-f]{64})/.*', + r'\1', url) + + class SWHBrowsableAPIRenderer(renderers.BrowsableAPIRenderer): """SWH's browsable api renderer. diff --git a/swh/web/ui/templates/revision-log.html b/swh/web/ui/templates/revision-log.html --- a/swh/web/ui/templates/revision-log.html +++ b/swh/web/ui/templates/revision-log.html @@ -3,133 +3,154 @@ {% block content %} {% if message is not none %} +
{{ message }} +
{% endif %} -
-

Queried revision:

- {% if sha1_git is not none %} -
Revision with git SHA1 {{ sha1_git }}
- {% else %} -
Origin ID {{ origin_id }}
-
Branch name {{ branch_name }}
- {% if timestamp is not none %} -
Time stamp {{ timestamp }}
- {% endif %} -
-
- {% endif %} -
+
+

Queried revision:

+ {% if sha1_git is not none %} +
Revision with git SHA1 {{ sha1_git }}
+ {% else %} +
Origin ID {{ origin_id }}
+
Branch name {{ branch_name }}
+ {% if timestamp is not none %} +
Time stamp {{ timestamp }}
+ {% endif %} + {% endif %} +
{% if revisions is not none %} +
{% for revision in revisions %} - -
- {% if revision['url'] is not none %} -
-
Revision
- + {% if revision['merge'] %} +
+
+ Merge +
+
+ {% for url in revision['parent_urls'] %} + {{ url | revision_id_from_url }} + {% endfor %}
- {% endif %} +
+ {% endif %} + + {% if revision['url'] is not none %} +
+
Revision
+ +
+ {% endif %} - {% if revision['history_url'] is not none %} -
-
Revision Log
- + {% if revision['history_url'] is not none %} +
+
Revision Log
+ +
+ {% endif %} + + {% if revision['history_context_url'] is not none %} +
+
Contextual Revision Log
+ +
+ {% endif %} + + {% if revision['directory_url'] is not none %} + + {% endif %} + + {% if revision['author'] is not none %} +
+
Author
+
+

+ {{ revision['author']['name'] }} + {% if 'decoding_failures' in revision['author'] %}(some decoding errors){% endif %} +

- {% endif %} +
+
+
Date
+

{{ revision['date'] }}

+
+ {% endif %} - {% if revision['history_context_url'] is not none %} -
-
Contextual Revision Log
- + {% if revision['committer'] is not none %} +
+
Committer
+
+

+ {{ revision['committer']['name'] }} + {% if 'decoding_failures' in revision['committer'] %}(some decoding errors){% endif %} +

- {% endif %} - - {% if revision['directory_url'] is not none %} - - {% endif %} +
+
+
Committer Date
+

{{ revision['committer_date'] }}

+
+ {% endif %} - {% if revision['author'] is not none %} -
-
Author
-
-

- {{ revision['author']['name'] }} - {% if 'decoding_failures' in revision['author'] %}(some decoding errors){% endif %} -

-
-
-
-
Date
-

{{ revision['date'] }}

-
- {% endif %} + {% if revision['message'] is not none %} +
+
Message
+
{{ revision['message'] }}
+
+ {% elif revision['message_encoding_failed'] %} +
+
Message
+ +
+ {% else %} +
+
Message
+
No message found.
+
+ {% endif %} - {% if revision['committer'] is not none %} -
-
Committer
-
-

- {{ revision['committer']['name'] }} - {% if 'decoding_failures' in revision['committer'] %}(some decoding errors){% endif %} -

-
-
-
-
Committer Date
-

{{ revision['committer_date'] }}

-
- {% endif %} + {% for key in revision.keys() %} + {% if key in ['type', 'synthetic'] and key not in ['decoding_failures'] and revision[key] is not none %} +
+
{{ key }}
+
{{ revision[key] }}
+
+ {% endif %} + {% endfor %} - {% if revision['message'] is not none %} -
-
Message
-
{{ revision['message'] }}
-
- {% elif revision['message_encoding_failed'] %} -
-
Message
- -
-
Message
-
No message found.
-
- {% endif %} - - {% for key in revision.keys() %} - {% if key in ['type', 'synthetic'] and key not in ['decoding_failures'] and revision[key] is not none %} -
-
{{ key }}
-

{{ revision[key] }}

-
- {% endif %} + {% for key in ['children_urls', 'parent_urls'] %} + {% if revision[key] is not none %} +
+
{{ key }}
+ {% for link in revision[key] %} + {% endfor %} - {% for key in ['parent_urls', 'children_urls'] %} - {% if revision[key] is not none %} -
-
{{ key }}
- {% for link in revision[key] %} - - {% endfor %} -
- {% endif %} - {% endfor %} - {% if 'decoding_failures' in revision %} -
-
(some decoding errors occurred)
-
- {% endif %} -
+ {% endif %} + {% endfor %} + + {% if 'decoding_failures' in revision %} +
+
(some decoding errors occurred)
+
+ {% endif %}
{% endfor %} - -{% endif %} - + + {% if next_revs_url is not none %} + + + Next revisions + + + + {% endif %} + {% endif %} +
+
{% endblock %} diff --git a/swh/web/ui/tests/test_converters.py b/swh/web/ui/tests/test_converters.py --- a/swh/web/ui/tests/test_converters.py +++ b/swh/web/ui/tests/test_converters.py @@ -344,6 +344,122 @@ '309d36484e7edf7bb912' }] }, + 'merge': True + } + + # when + actual_revision = converters.from_revision(revision_input) + + # then + self.assertEqual(actual_revision, expected_revision) + + @istest + def from_revision_nomerge(self): + revision_input = { + 'id': hashutil.hex_to_hash( + '18d8be353ed3480476f032475e7c233eff7371d5'), + 'parents': [ + hashutil.hex_to_hash( + '29d8be353ed3480476f032475e7c244eff7371d5') + ] + } + + expected_revision = { + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'parents': [ + '29d8be353ed3480476f032475e7c244eff7371d5' + ], + 'merge': False + } + + # when + actual_revision = converters.from_revision(revision_input) + + # then + self.assertEqual(actual_revision, expected_revision) + + @istest + def from_revision_noparents(self): + revision_input = { + 'id': hashutil.hex_to_hash( + '18d8be353ed3480476f032475e7c233eff7371d5'), + 'directory': hashutil.hex_to_hash( + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), + 'author': { + 'name': b'Software Heritage', + 'fullname': b'robot robot@softwareheritage.org', + 'email': b'robot@softwareheritage.org', + }, + 'committer': { + 'name': b'Software Heritage', + 'fullname': b'robot robot@softwareheritage.org', + 'email': b'robot@softwareheritage.org', + }, + 'message': b'synthetic revision message', + 'date': { + 'timestamp': datetime.datetime( + 2000, 1, 17, 11, 23, 54, + tzinfo=datetime.timezone.utc).timestamp(), + 'offset': 0, + 'negative_utc': False, + }, + 'committer_date': { + 'timestamp': datetime.datetime( + 2000, 1, 17, 11, 23, 54, + tzinfo=datetime.timezone.utc).timestamp(), + 'offset': 0, + 'negative_utc': False, + }, + 'synthetic': True, + 'type': 'tar', + 'children': [ + hashutil.hex_to_hash( + '123546353ed3480476f032475e7c244eff7371d5'), + ], + 'metadata': { + 'original_artifact': [{ + 'archive_type': 'tar', + 'name': 'webbase-5.7.0.tar.gz', + 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd', + 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1', + 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f' + '309d36484e7edf7bb912', + + }] + }, + } + + expected_revision = { + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'author': { + 'name': 'Software Heritage', + 'fullname': 'robot robot@softwareheritage.org', + 'email': 'robot@softwareheritage.org', + }, + 'committer': { + 'name': 'Software Heritage', + 'fullname': 'robot robot@softwareheritage.org', + 'email': 'robot@softwareheritage.org', + }, + 'message': 'synthetic revision message', + 'date': "2000-01-17T11:23:54+00:00", + 'committer_date': "2000-01-17T11:23:54+00:00", + 'children': [ + '123546353ed3480476f032475e7c244eff7371d5' + ], + 'type': 'tar', + 'synthetic': True, + 'metadata': { + 'original_artifact': [{ + 'archive_type': 'tar', + 'name': 'webbase-5.7.0.tar.gz', + 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd', + 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1', + 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f' + '309d36484e7edf7bb912' + }] + } } # when @@ -445,6 +561,7 @@ '309d36484e7edf7bb912' }] }, + 'merge': True } # when diff --git a/swh/web/ui/tests/test_service.py b/swh/web/ui/tests/test_service.py --- a/swh/web/ui/tests/test_service.py +++ b/swh/web/ui/tests/test_service.py @@ -14,6 +14,39 @@ from swh.web.ui.tests import test_app +REVISION_ID_BIN = hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5') +REVISION_ID = '18d8be353ed3480476f032475e7c233eff7371d5' +DIRECTORY_ID_BIN = hex_to_hash('7834ef7e7c357ce2af928115c6c6a42b7e2a44e6') +DIRECTORY_ID = '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6' +AUTHOR_ID_BIN = { + 'name': b'bill & boule', + 'email': b'bill@boule.org', +} +AUTHOR_ID = { + 'name': 'bill & boule', + 'email': 'bill@boule.org', +} +COMMITTER_ID_BIN = { + 'name': b'boule & bill', + 'email': b'boule@bill.org', +} +COMMITTER_ID = { + 'name': 'boule & bill', + 'email': 'boule@bill.org', +} +SAMPLE_DATE_RAW = { + 'timestamp': datetime.datetime( + 2000, 1, 17, 11, 23, 54, + tzinfo=datetime.timezone.utc, + ).timestamp(), + 'offset': 0, + 'negative_utc': False, +} +SAMPLE_DATE = '2000-01-17T11:23:54+00:00' +SAMPLE_MESSAGE_BIN = b'elegant fix for bug 31415957' +SAMPLE_MESSAGE = 'elegant fix for bug 31415957' + + class ServiceTestCase(test_app.SWHApiTestCase): @patch('swh.web.ui.service.backend') @@ -574,6 +607,7 @@ 'parents': [], 'children': [hash_to_hex(b'999'), hash_to_hex(b'777')], 'directory': hash_to_hex(b'278'), + 'merge': False }) mock_query.parse_hash_with_algorithms_or_throws.assert_has_calls( @@ -646,6 +680,7 @@ 'parents': [], 'children': [hash_to_hex(b'999'), hash_to_hex(b'777')], 'directory': hash_to_hex(b'278'), + 'merge': False }) mock_query.parse_hash_with_algorithms_or_throws.assert_called_once_with( # noqa @@ -953,34 +988,13 @@ def lookup_revision(self, mock_backend): # given mock_backend.revision_get = MagicMock(return_value={ - 'id': hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5'), - 'directory': hex_to_hash( - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), - 'author': { - 'name': b'bill & boule', - 'email': b'bill@boule.org', - }, - 'committer': { - 'name': b'boule & bill', - 'email': b'boule@bill.org', - }, - 'message': b'elegant fix for bug 31415957', - 'date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, - 'committer_date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, + 'id': REVISION_ID_BIN, + 'directory': DIRECTORY_ID_BIN, + 'author': AUTHOR_ID_BIN, + 'committer': COMMITTER_ID_BIN, + 'message': SAMPLE_MESSAGE_BIN, + 'date': SAMPLE_DATE_RAW, + 'committer_date': SAMPLE_DATE_RAW, 'synthetic': False, 'type': 'git', 'parents': [], @@ -989,31 +1003,26 @@ # when actual_revision = service.lookup_revision( - '18d8be353ed3480476f032475e7c233eff7371d5') + REVISION_ID) # then self.assertEqual(actual_revision, { - 'id': '18d8be353ed3480476f032475e7c233eff7371d5', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'author': { - 'name': 'bill & boule', - 'email': 'bill@boule.org', - }, - 'committer': { - 'name': 'boule & bill', - 'email': 'boule@bill.org', - }, - 'message': 'elegant fix for bug 31415957', - 'date': "2000-01-17T11:23:54+00:00", - 'committer_date': "2000-01-17T11:23:54+00:00", + 'id': REVISION_ID, + 'directory': DIRECTORY_ID, + 'author': AUTHOR_ID, + 'committer': COMMITTER_ID, + 'message': SAMPLE_MESSAGE, + 'date': SAMPLE_DATE, + 'committer_date': SAMPLE_DATE, 'synthetic': False, 'type': 'git', 'parents': [], 'metadata': [], + 'merge': False }) mock_backend.revision_get.assert_called_with( - hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5')) + REVISION_ID_BIN) @patch('swh.web.ui.service.backend') @istest @@ -1021,33 +1030,12 @@ # given stub_rev = { 'id': hex_to_hash('123456'), - 'directory': hex_to_hash( - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), - 'author': { - 'name': b'bill & boule', - 'email': b'bill@boule.org', - }, - 'committer': { - 'name': b'boule & bill', - 'email': b'boule@bill.org', - }, + 'directory': DIRECTORY_ID_BIN, + 'author': AUTHOR_ID_BIN, + 'committer': COMMITTER_ID_BIN, 'message': b'elegant fix for bug \xff', - 'date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, - 'committer_date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, + 'date': SAMPLE_DATE_RAW, + 'committer_date': SAMPLE_DATE_RAW, 'synthetic': False, 'type': 'git', 'parents': [], @@ -1057,66 +1045,40 @@ # when actual_revision = service.lookup_revision( - '18d8be353ed3480476f032475e7c233eff7371d5') + REVISION_ID) # then self.assertEqual(actual_revision, { 'id': '123456', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'author': { - 'name': 'bill & boule', - 'email': 'bill@boule.org', - }, - 'committer': { - 'name': 'boule & bill', - 'email': 'boule@bill.org', - }, + 'directory': DIRECTORY_ID, + 'author': AUTHOR_ID, + 'committer': COMMITTER_ID, 'message': None, 'message_decoding_failed': True, - 'date': "2000-01-17T11:23:54+00:00", - 'committer_date': "2000-01-17T11:23:54+00:00", + 'date': SAMPLE_DATE, + 'committer_date': SAMPLE_DATE, 'synthetic': False, 'type': 'git', 'parents': [], 'metadata': [], + 'merge': False }) mock_backend.revision_get.assert_called_with( - hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5')) + REVISION_ID_BIN) @patch('swh.web.ui.service.backend') @istest def lookup_revision_msg_ok(self, mock_backend): # given mock_backend.revision_get.return_value = { - 'id': hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5'), - 'directory': hex_to_hash( - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), - 'author': { - 'name': b'bill & boule', - 'email': b'bill@boule.org', - }, - 'committer': { - 'name': b'boule & bill', - 'email': b'boule@bill.org', - }, - 'message': b'elegant fix for bug 31415957', - 'date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, - 'committer_date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, + 'id': REVISION_ID_BIN, + 'directory': DIRECTORY_ID_BIN, + 'author': AUTHOR_ID_BIN, + 'committer': COMMITTER_ID_BIN, + 'message': SAMPLE_MESSAGE_BIN, + 'date': SAMPLE_DATE_RAW, + 'committer_date': SAMPLE_DATE_RAW, 'synthetic': False, 'type': 'git', 'parents': [], @@ -1125,45 +1087,24 @@ # when rv = service.lookup_revision_message( - '18d8be353ed3480476f032475e7c233eff7371d5') + REVISION_ID) # then - self.assertEquals(rv, {'message': b'elegant fix for bug 31415957'}) + self.assertEquals(rv, {'message': SAMPLE_MESSAGE_BIN}) mock_backend.revision_get.assert_called_with( - hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5')) + REVISION_ID_BIN) @patch('swh.web.ui.service.backend') @istest def lookup_revision_msg_absent(self, mock_backend): # given mock_backend.revision_get.return_value = { - 'id': hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5'), - 'directory': hex_to_hash( - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), - 'author': { - 'name': b'bill & boule', - 'email': b'bill@boule.org', - }, - 'committer': { - 'name': b'boule & bill', - 'email': b'boule@bill.org', - }, - 'date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, - 'committer_date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, + 'id': REVISION_ID_BIN, + 'directory': DIRECTORY_ID_BIN, + 'author': AUTHOR_ID_BIN, + 'committer': COMMITTER_ID_BIN, + 'date': SAMPLE_DATE_RAW, + 'committer_date': SAMPLE_DATE_RAW, 'synthetic': False, 'type': 'git', 'parents': [], @@ -1173,11 +1114,11 @@ # when with self.assertRaises(NotFoundExc) as cm: service.lookup_revision_message( - '18d8be353ed3480476f032475e7c233eff7371d5') + REVISION_ID) # then mock_backend.revision_get.assert_called_with( - hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5')) + REVISION_ID_BIN) self.assertEqual(cm.exception.args[0], 'No message for revision ' 'with sha1_git ' '18d8be353ed3480476f032475e7c233eff7371d5.') @@ -1191,11 +1132,11 @@ # when with self.assertRaises(NotFoundExc) as cm: service.lookup_revision_message( - '18d8be353ed3480476f032475e7c233eff7371d5') + REVISION_ID) # then mock_backend.revision_get.assert_called_with( - hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5')) + REVISION_ID_BIN) self.assertEqual(cm.exception.args[0], 'Revision with sha1_git ' '18d8be353ed3480476f032475e7c233eff7371d5 ' 'not found.') @@ -1205,37 +1146,19 @@ def lookup_revision_multiple(self, mock_backend): # given - sha1_bin = '18d8be353ed3480476f032475e7c233eff7371d5' + sha1_bin = REVISION_ID sha1_other = 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc' stub_revisions = [ { 'id': hex_to_hash(sha1_bin), - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'author': { - 'name': b'bill & boule', - 'email': b'bill@boule.org', - }, - 'committer': { - 'name': b'boule & bill', - 'email': b'boule@bill.org', - }, - 'message': b'elegant fix for bug 31415957', - 'date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc).timestamp(), - 'offset': 0, - 'negative_utc': False - }, + 'directory': DIRECTORY_ID, + 'author': AUTHOR_ID_BIN, + 'committer': COMMITTER_ID_BIN, + 'message': SAMPLE_MESSAGE_BIN, + 'date': SAMPLE_DATE_RAW, 'date_offset': 0, - 'committer_date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc).timestamp(), - 'offset': 0, - 'negative_utc': False - }, + 'committer_date': SAMPLE_DATE_RAW, 'committer_date_offset': 0, 'synthetic': False, 'type': 'git', @@ -1287,24 +1210,19 @@ self.assertEqual(list(actual_revisions), [ { 'id': sha1_bin, - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'author': { - 'name': 'bill & boule', - 'email': 'bill@boule.org', - }, - 'committer': { - 'name': 'boule & bill', - 'email': 'boule@bill.org', - }, - 'message': 'elegant fix for bug 31415957', - 'date': '2000-01-17T11:23:54+00:00', + 'directory': DIRECTORY_ID, + 'author': AUTHOR_ID, + 'committer': COMMITTER_ID, + 'message': SAMPLE_MESSAGE, + 'date': SAMPLE_DATE, 'date_offset': 0, - 'committer_date': '2000-01-17T11:23:54+00:00', + 'committer_date': SAMPLE_DATE, 'committer_date_offset': 0, 'synthetic': False, 'type': 'git', 'parents': [], 'metadata': [], + 'merge': False }, { 'id': sha1_other, @@ -1326,13 +1244,14 @@ 'type': 'git', 'parents': [], 'metadata': [], + 'merge': False } ]) self.assertEqual( list(mock_backend.revision_get_multiple.call_args[0][0]), [hex_to_hash( - '18d8be353ed3480476f032475e7c233eff7371d5'), + REVISION_ID), hex_to_hash( 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc')]) @@ -1340,7 +1259,7 @@ @istest def lookup_revision_multiple_none_found(self, mock_backend): # given - sha1_bin = '18d8be353ed3480476f032475e7c233eff7371d5' + sha1_bin = REVISION_ID sha1_other = 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc' mock_backend.revision_get_multiple.return_value = [] @@ -1354,7 +1273,7 @@ self.assertEqual( list(mock_backend.revision_get_multiple.call_args[0][0]), [hex_to_hash( - '18d8be353ed3480476f032475e7c233eff7371d5'), + REVISION_ID), hex_to_hash( 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc')]) @@ -1364,33 +1283,12 @@ # given stub_revision_log = [{ 'id': hex_to_hash('28d8be353ed3480476f032475e7c233eff7371d5'), - 'directory': hex_to_hash( - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), - 'author': { - 'name': b'bill & boule', - 'email': b'bill@boule.org', - }, - 'committer': { - 'name': b'boule & bill', - 'email': b'boule@bill.org', - }, - 'message': b'elegant fix for bug 31415957', - 'date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, - 'committer_date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, + 'directory': DIRECTORY_ID_BIN, + 'author': AUTHOR_ID_BIN, + 'committer': COMMITTER_ID_BIN, + 'message': SAMPLE_MESSAGE_BIN, + 'date': SAMPLE_DATE_RAW, + 'committer_date': SAMPLE_DATE_RAW, 'synthetic': False, 'type': 'git', 'parents': [], @@ -1405,22 +1303,17 @@ # then self.assertEqual(list(actual_revision), [{ 'id': '28d8be353ed3480476f032475e7c233eff7371d5', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'author': { - 'name': 'bill & boule', - 'email': 'bill@boule.org', - }, - 'committer': { - 'name': 'boule & bill', - 'email': 'boule@bill.org', - }, - 'message': 'elegant fix for bug 31415957', - 'date': "2000-01-17T11:23:54+00:00", - 'committer_date': "2000-01-17T11:23:54+00:00", + 'directory': DIRECTORY_ID, + 'author': AUTHOR_ID, + 'committer': COMMITTER_ID, + 'message': SAMPLE_MESSAGE, + 'date': SAMPLE_DATE, + 'committer_date': SAMPLE_DATE, 'synthetic': False, 'type': 'git', 'parents': [], 'metadata': [], + 'merge': False }]) mock_backend.revision_log.assert_called_with( @@ -1432,33 +1325,12 @@ # given stub_revision_log = [{ 'id': hex_to_hash('28d8be353ed3480476f032475e7c233eff7371d5'), - 'directory': hex_to_hash( - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), - 'author': { - 'name': b'bill & boule', - 'email': b'bill@boule.org', - }, - 'committer': { - 'name': b'boule & bill', - 'email': b'boule@bill.org', - }, - 'message': b'elegant fix for bug 31415957', - 'date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, - 'committer_date': { - 'timestamp': datetime.datetime( - 2000, 1, 17, 11, 23, 54, - tzinfo=datetime.timezone.utc, - ).timestamp(), - 'offset': 0, - 'negative_utc': False, - }, + 'directory': DIRECTORY_ID_BIN, + 'author': AUTHOR_ID_BIN, + 'committer': COMMITTER_ID_BIN, + 'message': SAMPLE_MESSAGE_BIN, + 'date': SAMPLE_DATE_RAW, + 'committer_date': SAMPLE_DATE_RAW, 'synthetic': False, 'type': 'git', 'parents': [], @@ -1473,22 +1345,17 @@ # then self.assertEqual(list(actual_log), [{ 'id': '28d8be353ed3480476f032475e7c233eff7371d5', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'author': { - 'name': 'bill & boule', - 'email': 'bill@boule.org', - }, - 'committer': { - 'name': 'boule & bill', - 'email': 'boule@bill.org', - }, - 'message': 'elegant fix for bug 31415957', - 'date': "2000-01-17T11:23:54+00:00", - 'committer_date': "2000-01-17T11:23:54+00:00", + 'directory': DIRECTORY_ID, + 'author': AUTHOR_ID, + 'committer': COMMITTER_ID, + 'message': SAMPLE_MESSAGE, + 'date': SAMPLE_DATE, + 'committer_date': SAMPLE_DATE, 'synthetic': False, 'type': 'git', 'parents': [], 'metadata': [], + 'merge': False }]) mock_backend.revision_log_by.assert_called_with( @@ -1756,8 +1623,7 @@ # given stub_rev = { 'id': hex_to_hash('28d8be353ed3480476f032475e7c233eff7371d5'), - 'directory': hex_to_hash( - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), + 'directory': DIRECTORY_ID_BIN, 'author': { 'name': b'ynot', 'email': b'ynot@blah.org', @@ -1785,7 +1651,7 @@ expected_rev = { 'id': '28d8be353ed3480476f032475e7c233eff7371d5', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory': DIRECTORY_ID, 'author': { 'name': 'ynot', 'email': 'ynot@blah.org', @@ -1811,6 +1677,81 @@ @patch('swh.web.ui.service.backend') @istest + def lookup_revision_by_nomerge(self, mock_backend): + # given + stub_rev = { + 'id': hex_to_hash('28d8be353ed3480476f032475e7c233eff7371d5'), + 'directory': DIRECTORY_ID_BIN, + 'author': { + 'name': b'ynot', + 'email': b'ynot@blah.org', + }, + 'parents': [ + hex_to_hash('adc83b19e793491b1c6ea0fd8b46cd9f32e592fc')] + } + + expected_rev = { + 'id': '28d8be353ed3480476f032475e7c233eff7371d5', + 'directory': DIRECTORY_ID, + 'author': { + 'name': 'ynot', + 'email': 'ynot@blah.org', + }, + 'parents': ['adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'], + 'merge': False + } + + mock_backend.revision_get_by.return_value = stub_rev + + # when + actual_revision = service.lookup_revision_by(10, 'master2', 'some-ts') + + # then + self.assertEquals(actual_revision, expected_rev) + + mock_backend.revision_get_by(1, 'master2', 'some-ts') + + @patch('swh.web.ui.service.backend') + @istest + def lookup_revision_by_merge(self, mock_backend): + # given + stub_rev = { + 'id': hex_to_hash('28d8be353ed3480476f032475e7c233eff7371d5'), + 'directory': DIRECTORY_ID_BIN, + 'author': { + 'name': b'ynot', + 'email': b'ynot@blah.org', + }, + 'parents': [ + hex_to_hash('adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'), + hex_to_hash('adc83b19e793491b1c6db0fd8b46cd9f32e592fc') + ] + } + + expected_rev = { + 'id': '28d8be353ed3480476f032475e7c233eff7371d5', + 'directory': DIRECTORY_ID, + 'author': { + 'name': 'ynot', + 'email': 'ynot@blah.org', + }, + 'parents': ['adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', + 'adc83b19e793491b1c6db0fd8b46cd9f32e592fc'], + 'merge': True + } + + mock_backend.revision_get_by.return_value = stub_rev + + # when + actual_revision = service.lookup_revision_by(10, 'master2', 'some-ts') + + # then + self.assertEquals(actual_revision, expected_rev) + + mock_backend.revision_get_by(1, 'master2', 'some-ts') + + @patch('swh.web.ui.service.backend') + @istest def lookup_revision_with_context_by_ko(self, mock_backend): # given mock_backend.revision_get_by.return_value = None diff --git a/swh/web/ui/tests/views/test_api.py b/swh/web/ui/tests/views/test_api.py --- a/swh/web/ui/tests/views/test_api.py +++ b/swh/web/ui/tests/views/test_api.py @@ -1443,10 +1443,10 @@ 'id': '18d8be353ed3480476f032475e7c233eff7371d5', 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233ef' - 'f7371d5/log/', + 'f7371d5/log/', 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6a' - '42b7e2a44e6/', + '42b7e2a44e6/', 'author_name': 'Software Heritage', 'author_email': 'robot@softwareheritage.org', 'committer_name': 'Software Heritage', @@ -1465,6 +1465,11 @@ 'synthetic': True, }] + expected_response = { + 'revisions': expected_revisions, + 'next_revs_url': None + } + # when rv = self.app.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6a42' 'b7e2a44e6/log/') @@ -1474,10 +1479,44 @@ self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) - self.assertEquals(response_data, expected_revisions) + self.assertEquals(response_data, expected_response) mock_service.lookup_revision_log.assert_called_once_with( - '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 100) + '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 26) + + @patch('swh.web.ui.views.api.service') + @istest + def api_revision_log_with_next(self, mock_service): + # given + stub_revisions = [] + for i in range(27): + stub_revisions.append({'id': i}) + + mock_service.lookup_revision_log.return_value = stub_revisions[:26] + + expected_revisions = [x for x in stub_revisions if x['id'] < 25] + for e in expected_revisions: + e['url'] = '/api/1/revision/%s/' % e['id'] + e['history_url'] = '/api/1/revision/%s/log/' % e['id'] + + expected_response = { + 'revisions': expected_revisions, + 'next_revs_url': '/api/1/revision/25/log/' + } + + # when + rv = self.app.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6a42' + 'b7e2a44e6/log/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv.mimetype, 'application/json') + + response_data = json.loads(rv.data.decode('utf-8')) + self.assertEquals(response_data, expected_response) + + mock_service.lookup_revision_log.assert_called_once_with( + '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 26) @patch('swh.web.ui.views.api.service') @istest @@ -1487,7 +1526,7 @@ # when rv = self.app.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6a42b7' - 'e2a44e6/log/?limit=10') + 'e2a44e6/log/') # then self.assertEquals(rv.status_code, 404) @@ -1499,7 +1538,7 @@ ' 8834ef7e7c357ce2af928115c6c6a42b7e2a44e6 not found.'}) mock_service.lookup_revision_log.assert_called_once_with( - '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 10) + '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 26) @patch('swh.web.ui.views.api.service') @istest @@ -1536,15 +1575,7 @@ 'synthetic': True, }] - # when - rv = self.app.get('/api/1/revision/18d8be353ed3480476f0' - '32475e7c233eff7371d5/prev/prev-rev/log/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv.mimetype, 'application/json') - response_data = json.loads(rv.data.decode('utf-8')) - self.assertEquals(response_data, [ + expected_revisions = [ { 'url': '/api/1/revision/' '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/', @@ -1592,10 +1623,25 @@ ], 'type': 'tar', 'synthetic': True, - }]) + }] + + expected_response = { + 'revisions': expected_revisions, + 'next_revs_url': None + } + + # when + rv = self.app.get('/api/1/revision/18d8be353ed3480476f0' + '32475e7c233eff7371d5/prev/prev-rev/log/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv.mimetype, 'application/json') + response_data = json.loads(rv.data.decode('utf-8')) + self.assertEquals(response_data, expected_response) mock_service.lookup_revision_log.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5', 100) + '18d8be353ed3480476f032475e7c233eff7371d5', 26) mock_service.lookup_revision_multiple.assert_called_once_with( ['prev-rev']) diff --git a/swh/web/ui/tests/views/test_browse.py b/swh/web/ui/tests/views/test_browse.py --- a/swh/web/ui/tests/views/test_browse.py +++ b/swh/web/ui/tests/views/test_browse.py @@ -913,27 +913,30 @@ @istest def browse_revision_log(self, mock_api): # given - stub_revisions = [{ - 'id': 'd770e558e21961ad6cfdf0ff7df0eb5d7d4f0754', - 'date': 'Sun, 05 Jul 2015 18:01:52 GMT', - 'committer': { - 'email': 'torvalds@linux-foundation.org', - 'name': 'Linus Torvalds' - }, - 'committer_date': 'Sun, 05 Jul 2015 18:01:52 GMT', - 'type': 'git', - 'author': { - 'email': 'torvalds@linux-foundation.org', - 'name': 'Linus Torvalds' - }, - 'message': 'Linux 4.2-rc1\n', - 'synthetic': False, - 'directory_url': '/api/1/directory/' - '2a1dbabeed4dcf1f4a4c441993b2ffc9d972780b/', - 'parent_url': [ - '/api/1/revision/a585d2b738bfa26326b3f1f40f0f1eda0c067ccf/' - ], - }] + stub_revisions = { + 'revisions': [{ + 'id': 'd770e558e21961ad6cfdf0ff7df0eb5d7d4f0754', + 'date': 'Sun, 05 Jul 2015 18:01:52 GMT', + 'committer': { + 'email': 'torvalds@linux-foundation.org', + 'name': 'Linus Torvalds' + }, + 'committer_date': 'Sun, 05 Jul 2015 18:01:52 GMT', + 'type': 'git', + 'author': { + 'email': 'torvalds@linux-foundation.org', + 'name': 'Linus Torvalds' + }, + 'message': 'Linux 4.2-rc1\n', + 'synthetic': False, + 'directory_url': '/api/1/directory/' + '2a1dbabeed4dcf1f4a4c441993b2ffc9d972780b/', + 'parent_url': [ + '/api/1/revision/a585d2b738bfa26326b3f1f40f0f1eda0c067ccf/' + ], + }], + 'next_revs_url': '/api/1/revision/1234/log/' + } mock_api.api_revision_log.return_value = stub_revisions # when @@ -945,6 +948,9 @@ self.assertEqual(self.get_context_variable('sha1_git'), '426') self.assertTrue( isinstance(self.get_context_variable('revisions'), map)) + self.assertEqual( + self.get_context_variable('next_revs_url'), + '/browse/revision/1234/log/') self.assertIsNone(self.get_context_variable('message')) mock_api.api_revision_log.assert_called_once_with('426', None) diff --git a/swh/web/ui/views/api.py b/swh/web/ui/views/api.py --- a/swh/web/ui/views/api.py +++ b/swh/web/ui/views/api.py @@ -705,7 +705,7 @@ @app.route('/api/1/revision//log/') @app.route('/api/1/revision//prev//log/') -def api_revision_log(sha1_git, prev_sha1s=None): +def api_revision_log(sha1_git, prev_sha1s=None, limit=25): """Show all revisions (~git log) starting from sha1_git. The first element returned is the given sha1_git. @@ -724,26 +724,42 @@ NotFoundExc if the revision is not found. """ - limit = int(request.args.get('limit', '100')) - def lookup_revision_log_with_limit(s, limit=limit): + response = {'revisions': None, 'next_revs_url': None} + revisions = None + next_revs_url = None + + def lookup_revision_log_with_limit(s, limit=limit+1): return service.lookup_revision_log(s, limit) error_msg = 'Revision with sha1_git %s not found.' % sha1_git - rev_backward = _api_lookup(sha1_git, - lookup_fn=lookup_revision_log_with_limit, - error_msg_if_not_found=error_msg, - enrich_fn=utils.enrich_revision) + rev_get = _api_lookup(sha1_git, + lookup_fn=lookup_revision_log_with_limit, + error_msg_if_not_found=error_msg, + enrich_fn=utils.enrich_revision) + + if len(rev_get) == limit+1: + rev_backward = rev_get[:-1] + next_revs_url = url_for('api_revision_log', + sha1_git=rev_get[-1]['id']) + else: + rev_backward = rev_get if not prev_sha1s: # no nav breadcrumbs, so we're done - return rev_backward - - rev_forward_ids = prev_sha1s.split('/') - rev_forward = _api_lookup(rev_forward_ids, - lookup_fn=service.lookup_revision_multiple, - error_msg_if_not_found=error_msg, - enrich_fn=utils.enrich_revision) - return rev_forward + rev_backward + revisions = rev_backward + + else: + rev_forward_ids = prev_sha1s.split('/') + rev_forward = _api_lookup(rev_forward_ids, + lookup_fn=service.lookup_revision_multiple, + error_msg_if_not_found=error_msg, + enrich_fn=utils.enrich_revision) + revisions = rev_forward + rev_backward + + response['revisions'] = revisions + response['next_revs_url'] = next_revs_url + + return response @app.route('/api/1/revision' diff --git a/swh/web/ui/views/browse.py b/swh/web/ui/views/browse.py --- a/swh/web/ui/views/browse.py +++ b/swh/web/ui/views/browse.py @@ -364,8 +364,12 @@ 'revisions': []} try: - revisions = api.api_revision_log(sha1_git, prev_sha1s) + revision_data = api.api_revision_log(sha1_git, prev_sha1s) + revisions = revision_data['revisions'] + next_revs_url = revision_data['next_revs_url'] env['revisions'] = map(utils.prepare_data_for_view, revisions) + env['next_revs_url'] = utils.prepare_data_for_view(next_revs_url) + except (NotFoundExc, BadInputExc) as e: env['message'] = str(e)